package org.activequant.core.util;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TreeSet;

import org.activequant.core.domainmodel.marketscanner.MarketScan;
import org.activequant.core.domainmodel.marketscanner.MarketScanSeries;
import org.activequant.core.domainmodel.marketscanner.MarketScanSeriesSpecification;
import org.activequant.core.domainmodel.marketscanner.ScanDataEntity;
import org.activequant.core.domainmodel.marketscanner.ScanDataTimeSeries;
import org.activequant.core.types.TimeStamp;
import org.activequant.util.exceptions.InvalidDateOrderException;
import org.activequant.util.exceptions.ValueNotFoundException;
public class ScanDataTimeSeriesUtils {
	/**
	 * Returns a <code>MarketScan.class</code> depending on whether the given series(T) is
	 * a MarketScanSeries
	 * @param <U>
	 * @param <T>
	 * @param series
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <U extends ScanDataEntity<U>, T extends ScanDataTimeSeries<U>>
	Class<U> getEntityClass(T series) {
		if(MarketScanSeries.class.isAssignableFrom(series.getClass())) {
			return (Class<U>) (Class<?>) MarketScan.class;
		}
		throw new IllegalArgumentException("Unknown series");
	}
	/**
	 * clones the given in(T...) (where T extends ScanDataTimeSeries)
	 * @param <U>
	 * @param <T>
	 * @param clazz
	 * @param in
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <U extends ScanDataEntity<U>, T extends ScanDataTimeSeries<U>> 
	T[] cloneSeries(Class<U> clazz, T... in) {
		T[] out = (T[]) Array.newInstance(in[0].getClass(), in.length);
		for (int i = 0; i < in.length; i++) {
			out[i] = (T) in[i].clone();
		}
		return out;
	}
	/**
	 * clears the elements of each ScanDataTimeSeries in the given series(T...)
	 * @param <U>
	 * @param <T>
	 * @param clazz
	 * @param series
	 */
	public static <U extends ScanDataEntity<U>, T extends ScanDataTimeSeries<U>> 
	void clearSeries(Class<U> clazz, T... series) {
		for(T timeSeries: series) {
			timeSeries.clear();
		}
	}
	
	/**
	 * will align two scandata timeseries. will remove days that are not present in one of the two series. 
	 * @param ts1
	 * @param ts2
	 */
	public static <M extends ScanDataEntity<M>, T extends ScanDataTimeSeries<M>> 
	void alignTimeSeries(Class<M> clazz, T ts1, T ts2) {
	    HashMap<String, Integer> entries = new HashMap<String, Integer>();
	
	    for (M c : ts1) {
	        entries.put(c.getTimeStamp().toString(), 1);
	    }
	
	    for (M c : ts2) {
	        if (entries.containsKey(c.getTimeStamp().toString()))
	            entries.put(c.getTimeStamp().toString(), 2);
	        else
	            entries.put(c.getTimeStamp().toString(), 1);
	    }
	
	    List<M> toBeDeleted = new ArrayList<M>();
	
	    // walk over the ts1 and remove all the unknown candles.
	    for (M c : ts1) {
	        if (entries.get(c.getTimeStamp().toString()) != 2) {
	            toBeDeleted.add(c);
	        }
	    }
	
	    // walk over the ts2 and remove all the unknown candles.
	    for (M c : ts2) {
	        if (entries.get(c.getTimeStamp().toString()) != 2) {
	            toBeDeleted.add(c);
	        }
	    }
	
	    for (M c : toBeDeleted) {
	        ts1.remove(c);
	        ts2.remove(c);
	    }
	}    
	/**
	 * returns a MarketScanSeries by merging the given series(MarketScanSeries...) (ordered by TimeStamp - newest at index 0 to oldest at the end).<br/>
	 * Its seriesSpecification(SeriesSpecification) is set with the given series[0](CandleSeries)
	 * @param series
	 * @return
	 */
	public static MarketScanSeries merge(MarketScanSeries... series) {
		Set<MarketScan> set = newSortedSet();
		for(MarketScanSeries cs : series) {
			set.addAll(cs);
		}
		MarketScanSeriesSpecification spec = new MarketScanSeriesSpecification(series[0].getMarketScanSeriesSpecification());
		MarketScanSeries result = new MarketScanSeries(spec, set.toArray(new MarketScan[] {}));    	
		assert(isOrderValid(result));    	
		return result; 
	}
	/**
	 * returns a new TreeSet&lt;T&gt; (where T extends MarketDataEntity) and elements are ordered by their Timestamps
	 * @param <T>
	 * @return
	 */
	private static <T extends ScanDataEntity<T>> Set<T> newSortedSet() {
		return new TreeSet<T>(new Comparator<T>() {
			public int compare(T c2, T c1) {
				// null-safe (just in case)
				if(c1.getTimeStamp() == null) {
					return c2.getTimeStamp() == null ? 0 : -1;
				} else if(c2.getTimeStamp() == null) {
					return 1;
				}				
				return c1.getTimeStamp().compareTo(c2.getTimeStamp());
			}
		});
	}
	/**
	 * Reverts the given timeSeries(ScanDataTimeSeries&lt;T&gt;) if it isn't in valid order (latest/newest data at index=0). If that doesn't help either, it throws an AssertionException.<br/>
	 * The elements(T extends ScanDataEntity&lt;T&gt;) in the given timeSeries(ScanDataTimeSeries&lt;T&gt;) should be ordered so that the latest/newest one is at index 0.
	 * @param <T>
	 * @param timeSeries
	 */
	public static <T extends ScanDataEntity<T>> void alignDateOrder(ScanDataTimeSeries<T> timeSeries) {
		// need to check if we need to reverse the order due to date stuff.
		if (timeSeries.size() > 1 && !isOrderValid(timeSeries)) {
			Collections.reverse(timeSeries);
		}
		assert(isOrderValid(timeSeries));
	}
	
	/**
	 * helper method to sort a ScanDataTimeSeries. 
	 * @param ts1
	 * @return
	 */
	public static <T extends ScanDataEntity<T>> 
	void sort(ScanDataTimeSeries<T> timeSeries) {
		Collections.sort(timeSeries);
		alignDateOrder(timeSeries);
	}
	
	/**
	 * marketScans[0] newest, marketScans[n] oldest<br/>
	 * returns whether the timeStamp(TimeStamp)'s of the given entities(ScanDataEntity&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n)
	 * @param candles
	 * @return
	 */
	public static boolean isOrderValid(ScanDataEntity<?> ... entities) {
		for(int i = 1; i < entities.length; i++) {
			if(entities[i-1].getTimeStamp().isBefore(entities[i].getTimeStamp())) {
				// invalid candle order found. 
				return false;
			}
		}
		return true;
	}
	/**
	 * returns whether the timeStamp(TimeStamp)'s of the given entities(ScanDataTimeSeries&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n)
	 * @param <T>
	 * @param entities
	 * @return
	 */
	public static <T extends ScanDataTimeSeries<?>> boolean isOrderValid(T entities) {
		return isOrderValid(entities.toArray(new ScanDataEntity[] {}));
	}
	/**
	 * Checks whether the timeStamp(TimeStamp)'s of the given entities(ScanDataEntity&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n).
	 * Otherwise it throws an InvalidDateOrderException.
	 * @param entities
	 * @throws InvalidDateOrderException
	 */
	public static void checkOrder(ScanDataEntity<?> ... entities) throws InvalidDateOrderException {
		if(!isOrderValid(entities)) throw new InvalidDateOrderException();
	}
	/**
	 * Checks whether the timeStamp(TimeStamp)'s of the given entities(List&lt;ScanDataEntity&lt;?&gt;&gt;) are ordered from newest(at index=0) to oldest(at index=n).
	 * Otherwise it throws an InvalidDateOrderException.
	 * @param entities
	 * @throws InvalidDateOrderException
	 */
	public static void checkOrder(List<ScanDataEntity<?>> entities) throws InvalidDateOrderException {
		if(!isOrderValid(entities.toArray(new ScanDataEntity[] {}))) throw new InvalidDateOrderException();
	}
	
	/**
	 * returns the position of the given date within the array.<br/>
	 * Returns the feasible position index(int) of the given date(TimeStamp) within the given series(&lt;T extends TimeSeries&lt;U&gt;&gt;) 
	 * (assuming it is ordered - latest at index 0, to oldest at the end)
	 * @param date
	 * @return
	 * @throws ValueNotFoundException
	 */
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	int getFeasibleTimeStampPosition(Class<U> clazz, T series, TimeStamp date, int startIndex) throws ValueNotFoundException {
	    ListIterator<U> iter = series.listIterator(startIndex);
		while (iter.hasNext()) {
			U next = iter.next();
			if (next.getTimeStamp().isEqual(date) || next.getTimeStamp().isBefore(date))
				return iter.previousIndex();
		}
		return -1;//there aren't any marketScans from before the 
		//throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	U getByFeasibleTimeStamp(Class<U> clazz, T series, TimeStamp date, int startIndex) throws ValueNotFoundException {
		return series.get(getFeasibleTimeStampPosition(clazz, series, date, startIndex));
	}
	/**
	 * returns the position of the given date within the array.<br/>
	 * Returns the feasible position index(int) of the given date(TimeStamp) within the given series(&lt;T extends TimeSeries&lt;U&gt;&gt;) 
	 * (assuming it is ordered - latest at index 0, to oldest at the end)
	 * @param date
	 * @return
	 * @throws ValueNotFoundException
	 */
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	int getFeasibleTimeStampPosition(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
	    ListIterator<U> iter = series.listIterator();
		while (iter.hasNext()) {
			U next = iter.next();
			if (next.getTimeStamp().isEqual(date) || next.getTimeStamp().isBefore(date))
				return iter.previousIndex();
		}
		throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	U getByFeasibleTimeStamp(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
		return series.get(getFeasibleTimeStampPosition(clazz, series, date));
	}
	
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	int getLastFeasiblePositionTimeStamp(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
		ListIterator<U> iter = series.listIterator();
		while (iter.hasNext()) {
			U element = iter.next();
			if (element.getTimeStamp().isEqual(date)) {
				return iter.previousIndex();
			} else if(element.getTimeStamp().isBefore(date)) {
				return iter.previousIndex()-1;
			}
		}
		throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	
	@SuppressWarnings("unchecked")
	public static <T extends ScanDataTimeSeries<U>, U extends ScanDataEntity<U>>
	T feasibleSubList(Class<U> clazz, T series, TimeStamp start, TimeStamp end) throws ValueNotFoundException {
		int startPos = getLastFeasiblePositionTimeStamp(clazz, series, start) + 1;
		int endPos = getFeasibleTimeStampPosition(clazz, series, end);
		return (T) series.subList(endPos, startPos);
	}
}
